Skip to content

Preserve target order in generated project#1619

Open
mirkokg wants to merge 10 commits into
yonaskolb:masterfrom
mirkokg:feature/target-ordering
Open

Preserve target order in generated project#1619
mirkokg wants to merge 10 commits into
yonaskolb:masterfrom
mirkokg:feature/target-ordering

Conversation

@mirkokg

@mirkokg mirkokg commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Targets in the generated .xcodeproj now appear in the order they are declared in the source spec, for both YAML and JSON specs. Previously they were always sorted alphabetically, regardless of source order. This affects the Xcode sidebar and xcodebuild -list output.

Motivation

XcodeGen always sorted targets alphabetically — both at parse time (Project.swift:184) and when writing the final xcodeproj (PBXProjGenerator.swift:326). The only workaround was renaming/prefixing targets (01_App, 02_Tests), which leaks into scheme names, product names, etc.

The reason alphabetical sort existed in the first place: XcodeGen uses Yams.load for YAML and JSONSerialization for JSON, both of which return a Swift Dictionary that discards source order. A deterministic sort was imposed to avoid nondeterministic output. Yams also offers Yams.compose, which returns a Node tree that preserves mapping key order — and because JSON is a valid subset of YAML 1.2, Yams.compose handles JSON specs too. This PR uses it for order extraction on both formats, while leaving the main JSONSerialization parse path intact.

Since Xcode by itself allows you to order targets the way you want I think we should also have an option in Xcodegen to do the same.

Design

  • Yaml.swift: new loadOrderedTargetNames(path:) helper uses Yams.compose to read the ordered keys under the top-level targets: mapping. Works for both .yml and .json specs. Returns [] when targets: is absent (e.g. pure include fragments that only contribute settings: / targetTemplates:).
  • SpecFile: carries a per-file targetDeclarationOrder: [String]. A new resolvedTargetDeclarationOrder() walks the include graph and concatenates orders, with first-occurrence-wins dedup — an include that declares a target keeps its slot even when the main spec overrides the target's content.
  • Project.init: accepts an optional targetDeclarationOrder: [String] = []. Before resolveProject runs, declaration keys are expanded through expandedTargetOrder to handle two transformations that happen in resolveProject:
    • name: rename — IncludedTarget with name: IncludedTargetNew contributes IncludedTargetNew.
    • Multiplatform expansion — Foo with platform: [iOS, macOS] contributes Foo_iOS and Foo_macOS, respecting platformPrefix/platformSuffix.
  • Target: extracted multiplatformTargetName(fromExpanded:key:platform:) so the multiplatform naming formula has a single source of truth — shared between resolveMultiplatformTargets and expandedTargetOrder.
  • Sort key: (orderIndex[name] ?? .max, name) using a precomputed [name: position] dictionary — declaration order first, alphabetical tie-break for anything unlisted.
  • PBXProjGenerator: no longer sorts. Pulls project.targets in order, then appends project.aggregateTargets (which remain alphabetical since they live under a separate YAML/JSON key). Aggregate targets don't participate in declaration ordering; they always come last.

Behaviour change

This is a behavior change for every project loaded from a spec file. Regenerating will reshuffle the Xcode sidebar and xcodebuild -list output to match source order.

Programmatically-built Project (memberwise init Project(name:, targets: [...])) already preserved the array order as given — no sort. That path is unchanged.

Example

targets:
  MyApp:
    type: application
    platform: iOS
  MyAppTests:
    type: bundle.unit-test
    platform: iOS
  MyAppUITests:
    type: bundle.ui-testing
    platform: iOS

→ Sidebar order: MyApp, MyAppTests, MyAppUITests. Equivalent JSON spec behaves identically.

Testing

All 76 tests pass. Changes:

  • Added Tests/Fixtures/target_ordering_test.yml and Tests/Fixtures/target_ordering_test.json, and SpecLoadingTests.testTargetDeclarationOrder with two cases — YAML and JSON — each loading a spec with non-alphabetical keys and asserting source order is preserved.
  • Rewrote PBXProjGeneratorTests.testTargetOrdering — 3 cases: declaration order honored from a YAML-parsed Project; order preserved verbatim in a programmatically-built Project; aggregate targets land after native targets.
  • Updated one pre-existing assertion in SpecLoadingTests.testSpecLoader (the paths_test.yml case, deep include chain) that hard-coded the old alphabetical order.
  • Regenerated Tests/Fixtures/CarthageProject, Tests/Fixtures/SPM, and Tests/Fixtures/TestProject pbxproj files to reflect the new order — these are checked-in generator outputs.

Docs

  • CHANGELOG.md — entry under "Next Version" → "Changed", noting the behavior change.

Test plan

  • swift build clean
  • swift test — 76/76 pass
  • Manual: generate a fixture project with non-alphabetical target declarations (YAML and JSON), confirm Xcode sidebar order and xcodebuild -list output match source

@mirkokg

mirkokg commented Apr 17, 2026

Copy link
Copy Markdown
Contributor Author

Hi @yonaskolb I added this change to preserve target sorting as declared in specification, previously targets were always alphabetically sorted:

  let allTargets: [PBXTarget] = targetObjects.valueArray + targetAggregateObjects.valueArray
  pbxProject.targets = allTargets
      .sorted { $0.name < $1.name }

I initially added new option flag to specify the order, but after some sleep and using this in my project, I had a (hopefully) better idea in mind 💡

I think this is much more intuitive for the user.

@mirkokg mirkokg changed the title Add targetOrdering option to control target order in generated project Preserve target declaration order from YAML in generated project Apr 23, 2026
@mirkokg mirkokg changed the title Preserve target declaration order from YAML in generated project Preserve target declaration order generated project Apr 23, 2026
@mirkokg mirkokg changed the title Preserve target declaration order generated project Preserve target declaration order in generated project Apr 23, 2026
@mirkokg mirkokg changed the title Preserve target declaration order in generated project Preserve target order in generated project May 19, 2026
@mirkokg

mirkokg commented May 26, 2026

Copy link
Copy Markdown
Contributor Author

@yonaskolb any questions, any suggestions about this?

@pyrtsa

pyrtsa commented May 27, 2026

Copy link
Copy Markdown

This would be a very welcome change!

@yonaskolb

yonaskolb commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Thanks for working on this—preserving declaration order is a useful improvement. I found two resolution paths where the declared order is currently lost.

First, expandedTargetOrder is calculated before resolveProject, so it cannot see platform or name values inherited from target templates. For example:

targets:
  Zebra:
    type: framework
    templates: [CrossPlatform]
  Apple:
    type: framework
    platform: iOS

targetTemplates:
  CrossPlatform:
    platform: [iOS, tvOS]

This generates the target order Apple, Zebra_iOS, Zebra_tvOS, rather than Zebra_iOS, Zebra_tvOS, Apple.

Second, targetDeclarationOrder is extracted from the original file contents, while the target dictionary subsequently expands environment variables in keys. A first target named ${FIRST_TARGET} with FIRST_TARGET=Zebra is therefore absent from the order map and moves behind a later Apple target.

Could the declaration keys be carried through the same variable, template, rename, and multiplatform transformations as the target dictionary? Regression tests covering a template-supplied platform array and an environment-expanded target key would catch both cases.

The declaration order was computed from raw spec keys before
resolveProject, so it missed platform/name values inherited from
target templates, and keys containing ${VARIABLES} never matched
the expanded target dictionary — both cases silently fell back to
alphabetical order.

Instead of pre-computing final names, stamp each raw target with a
__declarationIndex before resolution. The index rides through the
variable, template, rename and multiplatform transformations along
with the rest of the target dictionary, and is read back off the
resolved targets (keyed by resolved name) to sort them. Declaration
keys are expanded with the same variables as the dictionary keys so
${VAR} target names stay in the order map.

Order extraction no longer throws: a spec whose contents can't be
re-parsed for ordering just falls back to alphabetical instead of
failing generation.

Adds regression tests for a template-supplied platform array, a
template-supplied name, and an environment-expanded target key.
@mirkokg

mirkokg commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

@yonaskolb thanks for checking out my PR. I updated it to take the approach you suggested, the declaration keys are now carried through the same transformations as the target dictionary:

  • Each raw target is stamped with a synthetic __declarationIndex before resolveProject (same convention as isMultiPlatformTarget). The index rides through the variable, template, rename, and multiplatform transformations and is read back off the resolved targets to sort them. This fixes the template-supplied platform/name case and removed the name-recomputation logic entirely.
  • Declaration keys are expanded with the same variables as the dictionary keys, fixing the ${FIRST_TARGET} case.

Added regression tests for both of your examples, plus a template-supplied name and a fallback-to-alphabetical case. Full suite is green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants